How to calculate a duration that is needed to animate a view from position A to position B with a given timing function

A long time ago I had a problem to solve that I needed to know the exact time that will take to animate a view by x which might be in the middle of the desired position giving into account the timing function that the animation uses.

Here it is, an objective-c category:

//  CAMediaTimingFunction+Duration.h
#import <QuartzCore/QuartzCore.h>

@interface CAMediaTimingFunction (Duration)

- (NSTimeInterval)timeNeededToMoveByY:(CGFloat)yMove totalYMove:(CGFloat)totalYMove duration:(NSTimeInterval)duration;

@end

#import "CAMediaTimingFunction+Duration.h"

@implementation CAMediaTimingFunction (Duration)

- (NSTimeInterval)timeNeededToMoveBy:(CGFloat)move totalMove:(CGFloat)totalMove duration:(NSTimeInterval)duration {
    // Using reference animation calculate needed time according to used timing function
    CGFloat normalizedMove = move/totalMove;

    // View only just for calculation
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
    CALayer *referenceLayer = view.layer;
    referenceLayer.hidden = YES;
    referenceLayer.speed = 0.0;
    [[UIApplication sharedApplication].keyWindow addSubview:view];

    // Reference animation to calculate time
    CABasicAnimation *basicAnimation =  [CABasicAnimation animationWithKeyPath:@"frame"];
    basicAnimation.duration = 1.0;
    basicAnimation.timingFunction = self;
    basicAnimation.fromValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 1, 1)];
    basicAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(100, 0, 1, 1)];

    [view.layer addAnimation:basicAnimation forKey:@"evaluatorAnimation"];

    // Force to run run-loop to get the presentation layer
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]];

    NSUInteger n = 0;
    CGFloat a = 0.0;
    CGFloat b = 1.0;
    CGFloat tolerance = 0.005;
    CGFloat move = 0.0;
    CGFloat middle = 0.0;

    // Biselection algorithm
    while (n < 1000) {
        middle = (a + b)/2;
        referenceLayer.timeOffset = middle;
        // Refresh animation to get updated presentation layer
        [[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]];
        move = referenceLayer.presentationLayer.position.x / 100;

        if ((move - tolerance) <= normalizedMove && normalizedMove <= (move + tolerance))
            break;

        n += 1;
        if (normalizedMove < move)
            b = middle;
        else
            a = middle;
    }

    [view removeFromSuperview];
    return middle * duration;
}

@end
Tagged with: